package com.uparlange.project.flextris
{
	import com.uparlange.project.flextris.skin.FlextrisSkin;
	import com.uparlange.project.flextris.tetrominoes.IPiece;
	import com.uparlange.project.flextris.tetrominoes.JPiece;
	import com.uparlange.project.flextris.tetrominoes.LPiece;
	import com.uparlange.project.flextris.tetrominoes.OPiece;
	import com.uparlange.project.flextris.tetrominoes.SPiece;
	import com.uparlange.project.flextris.tetrominoes.TPiece;
	import com.uparlange.project.flextris.tetrominoes.ZPiece;
	import com.uparlange.project.flextris.util.RandomUtil;
	
	import flash.events.Event;
	import flash.events.KeyboardEvent;
	import flash.events.MouseEvent;
	import flash.events.TimerEvent;
	import flash.ui.Keyboard;
	import flash.utils.Timer;
	
	import mx.binding.utils.BindingUtils;
	import mx.binding.utils.ChangeWatcher;
	import mx.controls.Alert;
	import mx.core.IVisualElement;
	import mx.events.CloseEvent;
	import mx.events.FlexEvent;
	import mx.events.ResizeEvent;
	
	import spark.components.Button;
	import spark.components.Group;
	import spark.components.Label;
	import spark.components.SkinnableContainer;
	
	/**
	 * Tetris manager
	 * 
	 * @author Ugo Parlange
	 */ 
	public class Flextris extends SkinnableContainer
	{
		/**
		 * Shared Object
		 */
		public static const FLEXTRIS_SHARED_OBJECT : String = "FLEXTRIS_SHARED_OBJECT";
		
		/**
		 * States
		 */ 
		private static const NORMAL_STATE : String = "normal";
		private static const PLAYING_STATE : String = "playing";
		private static const PAUSE_STATE : String = "pause";
		private static const GAME_OVER_STATE : String = "gameover";
		
		/**
		 * Skin part that handle score value
		 */ 
		[SkinPart(required="true")]
		public var scoreValue : Label;
		
		/**
		 * Skin part that handle level value
		 */
		[SkinPart(required="true")]
		public var levelValue : Label;
		
		/**
		 * Skin part that handle line count
		 */
		[SkinPart(required="true")]
		public var lineValue : Label;
		
		/**
		 * Skin part that represents play field
		 */
		[SkinPart(required="true")]
		public var playField : Group;
		
		/**
		 * Skin part that represents next piece zone
		 */
		[SkinPart(required="true")]
		public var nextPieceZone : Group;
		
		/**
		 * Skin part that represents help button
		 */
		[SkinPart(required="true")]
		public var helpButton : Button;
		
		/**
		 * Current piece
		 */ 
		private var currentPiece : Piece;
		
		/**
		 * Next piece
		 */ 
		private var nextPiece : Piece;
		
		/**
		 * Ghost piece
		 */ 
		private var ghostPiece : Piece;
		
		/**
		 * Timer, speed control
		 */ 
		private var timer : Timer;
		
		/**
		 * Matrice, constraint manager
		 */ 
		private var matrice : Matrice;
		
		/**
		 * List of pieces that can be created
		 */ 
		private var pieceClasses : Array = [IPiece, JPiece, LPiece, OPiece, SPiece, TPiece, ZPiece];
		
		/**
		 * List of watchers
		 */ 
		private var watchers : Array = new Array();;
		
		/**
		 * Score, score manager
		 */ 
		private var score : Score;
		
		/**
		 * To know if ghost piece is enabled (visible)
		 */ 
		private var ghostPieceEnabled : Boolean = false;
		
		/**
		 * Constructor
		 */ 
		public function Flextris() {
			super();
			init();
		}
		
		/**
		 * Override attachSkin
		 */ 
		override protected function attachSkin() : void {
			super.attachSkin();
			
			// show title screen
			showTitleScreen();
		}
		
		/**
		 * Override getCurrentSkinState
		 */ 
		override protected function getCurrentSkinState() : String {
			return skin.currentState;
		}
		
		/**
		 * Override partAdded
		 */ 
		override protected function partAdded(partName : String, instance : Object) : void {
			super.partAdded(partName, instance);
			
			if(instance == scoreValue) {
				watchers["score"] = BindingUtils.bindProperty(scoreValue, "text", score, "score");
			} else if(instance == levelValue) {
				watchers["level"] = BindingUtils.bindProperty(levelValue, "text", score, "level");
			} else if(instance == lineValue) {
				watchers["lines"] = BindingUtils.bindProperty(lineValue, "text", score, "lines");
			} else if(instance == helpButton) {
				helpButton.addEventListener(MouseEvent.CLICK, showHelp);
			} else if(instance == playField) {
				playField.addEventListener(ResizeEvent.RESIZE, playFieldResizedHandler);
			}
		}
		
		/**
		 * Override partRemoved
		 */ 
		override protected function partRemoved(partName : String, instance : Object) : void {
			super.partRemoved(partName, instance);
			
			var cw : ChangeWatcher;
			if(instance == scoreValue) {
				cw = watchers["score"] as ChangeWatcher
				cw.unwatch();
				cw = null;
			} else if(instance == levelValue) {
				cw = watchers["level"] as ChangeWatcher
				cw.unwatch();
				cw = null;
			} else if(instance == lineValue) {
				cw = watchers["lines"] as ChangeWatcher
				cw.unwatch();
				cw = null;
			} else if(instance == helpButton) {
				helpButton.removeEventListener(MouseEvent.CLICK, showHelp);
			} else if(instance == playField) {
				playField.removeEventListener(ResizeEvent.RESIZE, playFieldResizedHandler);
			}
		}
		
		private function playFieldResizedHandler(anEvent:ResizeEvent) : void {
			/*
			var ive:IVisualElement;
			if(currentPiece) {
				currentPiece.refresh(getSquareWidth(), getSquareHeight());
			}
			for(var i:int=0; i < playField.numElements; i++) {
				ive = playField.getElementAt(i) as IVisualElement;
				if(ive is PiecePart) {
					(ive as PiecePart).refresh(getSquareWidth(), getSquareHeight());
				}
			}
			*/
		}
		
		/**
		 * Start a new game
		 */ 
		public function play() : void {
			// init flextris
			initFlextris();
			// init state
			setCurrentSkinState(PLAYING_STATE)
			// init music
			JukeBox.getInstance().playMusicA();
			// start timer
			timer.start();
		}
		
		/**
		 * Stop current game
		 */ 
		public function stop() : void {
			showTitleScreen();
		}
		
		/**
		 * Play / pause current game
		 */ 
		public function playPause() : void {
			if(getCurrentSkinState() == PLAYING_STATE || getCurrentSkinState() == PAUSE_STATE) {
				// update state
				setCurrentSkinState((getCurrentSkinState() == PLAYING_STATE) ? PAUSE_STATE : PLAYING_STATE);
				// music
				JukeBox.getInstance().playPauseMusic();
				// timer
				if(timer.running) {
					timer.stop();
				} else {
					timer.start();
				}
			}
		}
		
		/**
		 * Level up
		 */ 
		private function levelUp() : void {
			if(getCurrentSkinState() == PLAYING_STATE) {
				score.levelUp();
			}
		}
		
		/**
		 * Hard drop current piece
		 */ 
		private function hardDrop() : void {
			if(getCurrentSkinState() == PLAYING_STATE && currentPiece != null) {
				while(currentPiece.canMoveBottom(matrice)) {
					currentPiece.moveBottom(matrice);
				}
			}
		}
		
		/**
		 * Move current piece right
		 */ 
		private function moveCurrentPieceRight() : void {
			if(getCurrentSkinState() == PLAYING_STATE && currentPiece != null) {
				// move current piece
				currentPiece.moveRight(matrice);
				// update Ghost piece
				updateGhostPiece();
			}
		}
		
		/**
		 * Move current piece left
		 */ 
		private function moveCurrentPieceLeft() : void {
			if(getCurrentSkinState() == PLAYING_STATE && currentPiece != null) {
				// move current piece
				currentPiece.moveLeft(matrice);
				// update Ghost piece
				updateGhostPiece();
			}
		}
		
		/**
		 * Rotate current piece counterclockwise
		 */ 
		private function rotateCurrentPieceCounterclockwise() : void {
			if(getCurrentSkinState() == PLAYING_STATE && currentPiece != null) {
				// rotate current piece
				currentPiece.rotate(matrice, false);
				// update Ghost piece
				updateGhostPiece();
			}
		}
		
		/**
		 * Rotate current piece clockwise
		 */ 
		private function rotateCurrentPieceClockwise() : void {
			if(getCurrentSkinState() == PLAYING_STATE && currentPiece != null) {
				// rotate current piece
				currentPiece.rotate(matrice);
				// update Ghost piece
				updateGhostPiece();
			}
		}
		
		/**
		 * Move current piece bottom
		 * 
		 * @param aPlaySound Boolean, to know if have to play a sound at moving
		 */ 
		private function moveCurrentPieceBottom(aPlaySound : Boolean = false) : void {
			if(getCurrentSkinState() == PLAYING_STATE && currentPiece != null) {
				// move current piece
				currentPiece.moveBottom(matrice, aPlaySound);
				// update Ghost piece
				updateGhostPiece();
			}
		}
		
		/**
		 * Display / remove ghost piece
		 */ 
		private function updateGhostPiece() : void {
			try {
				playField.removeElement(ghostPiece);
			} catch(e : Error) {
				// Nothing
			}
			if(ghostPieceEnabled) {
				ghostPiece = currentPiece.getGhostPiece(matrice);
				ghostPiece.addEventListener(FlexEvent.CREATION_COMPLETE, ghostPieceCreationCompleteHandler);
				playField.addElement(ghostPiece);
			}
		}
		
		/**
		 * Called when ghost piece created
		 * 
		 * @param anEvent the FlexEvent
		 */ 
		private function ghostPieceCreationCompleteHandler(anEvent:FlexEvent) : void {
			ghostPiece.removeEventListener(FlexEvent.CREATION_COMPLETE, ghostPieceCreationCompleteHandler);
			ghostPiece.setCurrentSkinState(currentPiece.skin.currentState);
			while(ghostPiece.canMoveBottom(matrice)) {
				ghostPiece.moveBottom(matrice);
			}
		}
		
		/**
		 * Manage keyboard events
		 * 
		 * @param anEvent KeyboardEvent, the key pressed
		 */ 
		private function keyPressed(anEvent : KeyboardEvent) : void {
			switch(anEvent.keyCode) {
				case Keyboard.RIGHT :
					moveCurrentPieceRight();
					break;
				case Keyboard.LEFT :
					moveCurrentPieceLeft();
					break;
				case Keyboard.DOWN :
					moveCurrentPieceBottom(true);
					break;
				case Keyboard.X :
					rotateCurrentPieceCounterclockwise();
					break;
				case Keyboard.C :
				case Keyboard.UP :
					rotateCurrentPieceClockwise();
					break;
				case Keyboard.N :
					play();
					break;
				case Keyboard.P :
					playPause();
					break;
				case Keyboard.T :
					stop();
					break;
				case Keyboard.S :
					showScores();
					break;
				case Keyboard.A :
				case Keyboard.B :
				case Keyboard.C :
					changeMusic(anEvent.keyCode);
					break;
				case Keyboard.SPACE :
					hardDrop();
					break;
				case Keyboard.NUMPAD_ADD :
					levelUp();
					break;
				case Keyboard.G :
					enableDisableGhostPiece();
					break;
			}
		}
		
		/**
		 * Enable / disable ghost piece visibility
		 */ 
		private function enableDisableGhostPiece() : void {
			ghostPieceEnabled = !ghostPieceEnabled;
		}
		
		/**
		 * Change current game music
		 * 
		 * @param aKeyCode uint, the key pressed
		 */ 
		private function changeMusic(aKeyCode : uint) : void {
			if(getCurrentSkinState() == PLAYING_STATE) {
				switch(aKeyCode) {
					case Keyboard.A : 
						JukeBox.getInstance().playMusicA(); 
						break;
					case Keyboard.B : 
						JukeBox.getInstance().playMusicB(); 
						break;
					case Keyboard.C : 
						JukeBox.getInstance().playMusicC(); 
						break;
				}
			}
		}
		
		/**
		 * Called to show title screen
		 */ 
		private function showTitleScreen() : void {
			// init flextris
			initFlextris();
			// init state
			setCurrentSkinState(NORMAL_STATE);
			// init music
			JukeBox.getInstance().playTitleScreen();
		}
		
		/**
		 * Display score list
		 */ 
		private function showScores() : void {
			if(getCurrentSkinState() == NORMAL_STATE) {
				// display popup
				var title : String = "Top " + Score.MAX_BEST_SCORES_TO_SAVE + " Scores";
				var message : String = score.getScoreList();
				Alert.show(message, title, Alert.OK, null, scorePopupCloseHandler, null, Alert.OK, null);
				// music
				JukeBox.getInstance().playScore();
			}
		}
		
		/**
		 * Called when score popup close
		 */ 
		private function scorePopupCloseHandler(anEvent : CloseEvent) : void {
			// init music
			JukeBox.getInstance().playTitleScreen();
		}
		
		/**
		 * Display help popup
		 * 
		 * @param anEvent Event, the handled event
		 */ 
		private function showHelp(anEvent : Event = null) : void {
			var title : String = "Help";
			var message : String = "";
			message += "============ Actions ============";
			message += "\n";
			message += "N = start new game";
			message += "\n";
			message += "P = pause/play current game";
			message += "\n";
			message += "T = stop current game";
			message += "\n";
			message += "S = show scores";
			message += "\n";
			message += "NUMPAD+ = level up";
			message += "\n";
			message += "G = enable/disable ghost piece";
			message += "\n";
			message += "=========== Movements ===========";
			message += "\n";
			message += "LEFT = move piece left";
			message += "\n";
			message += "RIGHT = move piece right";
			message += "\n";
			message += "DOWN = move piece down (soft drop)";
			message += "\n";
			message += "UP or C = rotate piece clockwise";
			message += "\n";
			message += "X = rotate piece counterclockwise";
			message += "\n";
			message += "SPACE = hard drop piece";
			message += "\n";
			message += "============= Music =============";
			message += "\n";
			message += "A = music A";
			message += "\n";
			message += "B = music B";
			message += "\n";
			message += "C = music C";
			Alert.show(message, title, Alert.OK, null, null, null, Alert.OK, null);
		}
		
		/**
		 * Initialize the Flextris
		 */ 
		private function init() : void {
			// skin
			setStyle("skinClass", FlextrisSkin);
			// listener
			addEventListener(KeyboardEvent.KEY_DOWN, keyPressed);
			// score
			score = new Score();
			// matrice
			matrice = new Matrice();
			// timer
			timer = new Timer(1000);
			timer.addEventListener(TimerEvent.TIMER, timerEvent);
			BindingUtils.bindProperty(timer, "delay", score, "speed");
		}
		
		/**
		 * Called to init Flextris
		 */ 
		private function initFlextris() : void {
			// stop timer
			timer.stop();
			// init play field
			nextPiece = null;
			if(nextPieceZone != null) {
				nextPieceZone.removeAllElements();
			}
			currentPiece = null;
			if(playField != null) {
				playField.removeAllElements();
			}
			// init ghost piece state
			ghostPieceEnabled = false;
			// init matrice
			matrice.raz();
			// init score
			score.raz();
		}
		
		/**
		 * Called each time timer handle
		 * 
		 * @param anEvent TimerEvent, the handled event
		 */ 
		private function timerEvent(anEvent : TimerEvent) : void {
			if(currentPiece == null) {
				// add new piece
				addNewPiece();
			} else if(!currentPiece.canMoveBottom(matrice)) {
				// play sound
				JukeBox.getInstance().playFinisDropSound();
				// add parts
				addCurrentPieceParts();
				// check complete lines
				checkCompleteLines();
				// add new piece
				addNewPiece();
			}
			// move current piece bottom
			moveCurrentPieceBottom();
		}
		
		/**
		 * Check number of complete lines
		 */ 
		private function checkCompleteLines() : void {
			var parts : Array;
			var piecePart : PiecePart;
			// Calculate number of completed lines
			var completeLines : Array = matrice.getCompleteLines();
			// check if complete lines
			if(completeLines.length > 0) {
				// update lineCount
				score.addLines(completeLines.length);
				// play sound
				JukeBox.getInstance().playLineComplete();
				// remove lines and move parts
				while(completeLines.length > 0) {
					// take last one
					var lineIndexToRemove : int = completeLines[completeLines.length - 1] as int;
					// remove line
					parts = getParts(lineIndexToRemove);
					for each(piecePart in parts) {
						matrice.unBook((playField.removeElement(piecePart) as PiecePart).position);
					}
					// move top lines
					for(var lineToMoveBottom : int = (lineIndexToRemove - 1); lineToMoveBottom >=0; lineToMoveBottom--) {
						parts = getParts(lineToMoveBottom);
						for each(piecePart in parts) {
							matrice.unBook(piecePart.position);
							piecePart.moveBottom(matrice);
							matrice.book(piecePart.position);
						}
					}
					// check if other lines to remove
					completeLines = matrice.getCompleteLines();
				}
			}
		}
		
		/**
		 * Gets all parts of a line index
		 * 
		 * @param aLineIndex int, the line index
		 * 
		 * @return Array, the list of completed lines
		 */ 
		private function getParts(aLineIndex : int = -1) : Array {
			var parts : Array = new Array();
			var piecePart : PiecePart;
			for (var i : int = 0; i < playField.numElements; i++) {
				piecePart = playField.getElementAt(i) as PiecePart;
				if(piecePart) {
					if(aLineIndex == -1 || aLineIndex == piecePart.position.y) {
						parts.push(piecePart);
					}
				}
			}
			return parts;
		}
		
		/**
		 * Add current block parts to the playfield
		 */ 
		private function addCurrentPieceParts() : void {
			var piecePart : PiecePart;
			var pieceToRemove : Piece = playField.removeElement(currentPiece) as Piece;
			for each(piecePart in pieceToRemove.getParts()) {
				matrice.book(piecePart.position);
				playField.addElement(piecePart);
			}
		}
		
		/**
		 * Gets a random piece class
		 * 
		 * @return Class, the randomed class
		 */ 
		private function getRamdomPieceClass() : Class {
			return pieceClasses[RandomUtil.getRandInRange(pieceClasses.length)];
		}
		
		/**
		 * Add new piece to the playfield
		 */ 
		private function addNewPiece() : void {
			var pieceClass : Class;
			// current piece
			if(nextPiece == null) {
				pieceClass = getRamdomPieceClass();
				currentPiece = new pieceClass() as Piece;
			} else {
				currentPiece = nextPieceZone.removeElement(nextPiece) as Piece;
			}
			currentPiece.lineIndex = -1;
			currentPiece.columnIndex = 3;
			playField.addElement(currentPiece);
			// check if game over
			if(!currentPiece.canMoveBottom(matrice)) {
				gameOver();
			} else {
				// next piece
				pieceClass = getRamdomPieceClass();
				nextPiece = new pieceClass() as Piece;
				nextPieceZone.addElement(nextPiece);
			}
		}
		
		/**
		 * Called to set skin current state
		 * 
		 * @param aState String, the state to set
		 */ 
		private function setCurrentSkinState(aState : String) : void {
			skin.setCurrentState(aState);
		}
		
		/**
		 * Called when game over occurs
		 */ 
		private function gameOver() : void {
			if(getCurrentSkinState() == PLAYING_STATE) {
				// stop timer
				timer.stop();
				// update state
				setCurrentSkinState(GAME_OVER_STATE);
				// music
				JukeBox.getInstance().stopMusic();
				JukeBox.getInstance().playGameOver();
				// save current score
				score.saveCurrent();
			}
		}
		
		/**
		 * Gets square width
		 * 
		 * @return Number, the square width
		 */ 
		private function getSquareWidth() : Number {
			return playField.width / Matrice.NB_SQUARE_WIDTH;
		}
		
		/**
		 * Gets square height
		 * 
		 * @return Number, the square height
		 */ 
		private function getSquareHeight() : Number {
			return playField.height / Matrice.NB_SQUARE_HEIGHT;
		}		
	}
}